﻿
using System;
using System.Linq;
using System.Text.RegularExpressions;
using PsqlDotnet.Enums;
namespace PsqlDotnet.Model.Schema.Parsers
{
    public class SchemaParser : ISchemaParser
    {
        static Regex nullableRegex = new Regex(@"^(not null)(.*)");

        static Regex foreignKeyRegex = new Regex(@"FOREIGN KEY \((?:(?:(?<source_columns>\w+),?)+)\) REFERENCES (?<ref_table>.+)" +
                                                 @"\((?:(?:(?<ref_columns>\w+),?)+)\)\s*(?<full_match>MATCH FULL)?\s*(?<partial_match>MATCH PARTIAL)?"+
                                                 @"\s*(?<simple_match>MATCH SIMPLE)?\s*(?<delete_actions>"+
                                                 @"ON DELETE (?<delete_action_type>(NO ACTION)?(RESTRICT)?(CASCADE)?(SET NULL)?(SET DEFAULT)?))?"+
                                                 @"\s*(?<update_actions>ON UPDATE (?<update_action_type>(NO ACTION)?(RESTRICT)?(CASCADE)?(SET NULL)?(SET DEFAULT)?))?");
        static Regex primaryKeyRegex = new Regex(@"PRIMARY KEY \((?:(?<column>\w+),?)+\).*");
        static Regex uniqueRegex = new Regex(@"^(UNIQUE) \((?<source_column>.+)\)$");

        static Regex characterVaryingRegex = new Regex(@"^(character varying)(\((?<field_size>\d+)\))?$");
        static Regex bitRegex = new Regex(@"^(bit)(\((?<field_size>\d+)\))?$");
        static Regex bitVaryingRegex = new Regex(@"^(bit varying)(\((?<field_size>\d+)\))?$");
        static Regex charRegex = new Regex(@"^(character)(\((?<field_size>\d+)\))?$");
        static Regex numericRegex = new Regex(@"^(numeric)(\((?<field_size>\d+),(?<precision>\d+)\))?$");
        static Regex timeRegex = new Regex(@"^(time)(\((?<field_size>\d+)\))?( with time zone)?$");
        static Regex timeWtzRegex = new Regex(@"^(time)(\((?<field_size>\d+)\))?( without time zone)?$");
        static Regex timestampRegex = new Regex(@"^(timestamp)(\((?<field_size>\d+)\))?( with time zone)?$");
        static Regex timestampWtzRegex = new Regex(@"^(timestamp)(\((?<field_size>\d+)\))?( with time zone)?$");

        static Regex serialRefex = new Regex(@"nextval\((?<sequence_name>.+)::regclass\)$");

        #region ISchemaParser Members

        public ColumnDataInfo Parse(string str, string nullableInfo)
        {
            ColumnDataInfo result = new ColumnDataInfo();

            ParseFixedDataTypes(str, nullableInfo, result);

            ParseVariableDataTypes(str, result);

            result.IsNullable = ParseIsNullable(nullableInfo);

            return result;
        }

        public IConstraint ParseConstraint(string tableName, string constraintName, string constraintInfo)
        {
            try
            {
                Match m = primaryKeyRegex.Match(constraintInfo);
                if (m.Success)
                {
                    var columnsGroup = m.Groups["column"];
                    if (columnsGroup != null && columnsGroup.Captures != null)
                    {
                        var columnNames = columnsGroup.Captures.Cast<Capture>().
                                            Select(c => c.Value).ToList();

                        return new PrimaryKey(constraintName, columnNames, tableName);
                    }
                }

                m = foreignKeyRegex.Match(constraintInfo);
                if (m.Success)
                {
                    ForeignKeySettings settings = new ForeignKeySettings()
                    {
                        SourceTable = tableName,
                        Name = constraintName
                    };

                    var sourceColumnsGroup = m.Groups["source_columns"];
                    if (sourceColumnsGroup != null)
                    {
                        settings.SourceColumns = sourceColumnsGroup.Captures.Cast<Capture>().
                            Select(c => c.Value).ToList();
                    }

                    settings.ReferencedTable = m.Groups["ref_table"].Value;

                    var referencedColumnsGroup = m.Groups["ref_columns"];
                    if (referencedColumnsGroup != null)
                    {
                        settings.ReferencedColumns = referencedColumnsGroup.Captures.Cast<Capture>().
                            Select(c => c.Value).ToList();
                    }


                    var deleteActionsGroup = m.Groups["delete_actions"];
                    if (deleteActionsGroup.Captures.Count > 0)
                    {
                        var deleteAction = ParseForeignKeyAction(m.Groups["delete_action_type"].Value);
                        settings.DeleteAction = deleteAction;
                    }

                    var updateActionsGroup = m.Groups["update_actions"];
                    if (updateActionsGroup.Captures.Count > 0)
                    {
                        var updateAction = ParseForeignKeyAction(m.Groups["update_action_type"].Value);
                        settings.UpdateAction = updateAction;
                    }

                    ForeignKey fKey = new ForeignKey(settings);
                    return fKey;
                }

                m = uniqueRegex.Match(constraintInfo);
                if (m.Success)
                    return new UniqueConstraint(constraintName,
                        m.Groups["source_column"].Value, tableName);
            }
            catch { }

            return null;
        }

        #endregion

        #region Private methods

        private ForeignKeyActionEnum ParseForeignKeyAction(string str)
        {
            switch (str.ToUpper())
            {
                case "RESTRICT":
                    return ForeignKeyActionEnum.Restrict;
                case "CASCADE":
                    return ForeignKeyActionEnum.Cascade;
                case "SET NULL":
                    return ForeignKeyActionEnum.SetNull;
                case "SET DEFAULT":
                    return ForeignKeyActionEnum.SetDefault;
                default:
                    return ForeignKeyActionEnum.NoAction;
            }
        }

        private void SetDataType(ColumnDataInfo info, DbDataTypeEnum dataType)
        {
            info.DataType = dataType;
        }

        private bool ParseIsNullable(string str)
        {
            if (String.IsNullOrWhiteSpace(str))
                return true; // nullable

            Match m = nullableRegex.Match(str);
            if (m.Success)
            {
                return false;
            }

            throw new ArgumentException("Illegal nullable column format");
        }

        private bool CheckSerialDataType(string str)
        {
            return serialRefex.IsMatch(str);
        }

        private void ParseFixedDataTypes(string str, string nullableInfo, ColumnDataInfo result)
        {
            switch (str.Trim().ToLower())
            {
                case "integer":
                    {
                        if (CheckSerialDataType(nullableInfo))
                            SetDataType(result, DbDataTypeEnum.Serial);
                        else
                            SetDataType(result, DbDataTypeEnum.Interval);
                    }
                    break;
                case "date":
                    SetDataType(result, DbDataTypeEnum.Date);
                    break;

                case "bigint":
                    if (CheckSerialDataType(nullableInfo))
                        SetDataType(result, DbDataTypeEnum.Bigserial);
                    else
                        SetDataType(result, DbDataTypeEnum.Bigint);
                    break;
                case "boolean":
                    SetDataType(result, DbDataTypeEnum.Boolean);
                    break;
                case "box":
                    SetDataType(result, DbDataTypeEnum.Box);
                    break;
                case "bytea":
                    SetDataType(result, DbDataTypeEnum.Bytea);
                    break;
                case "cidr":
                    SetDataType(result, DbDataTypeEnum.Cidr);
                    break;
                case "circle":
                    SetDataType(result, DbDataTypeEnum.Circle);
                    break;
                case "double precision":
                    SetDataType(result, DbDataTypeEnum.DoublePrecision);
                    break;
                case "inet":
                    SetDataType(result, DbDataTypeEnum.Inet);
                    break;
                case "line":
                    SetDataType(result, DbDataTypeEnum.Line);
                    break;
                case "lseg":
                    SetDataType(result, DbDataTypeEnum.Lseg);
                    break;
                case "macaddr":
                    SetDataType(result, DbDataTypeEnum.Macaddr);
                    break;
                case "money":
                    SetDataType(result, DbDataTypeEnum.Money);
                    break;
                case "path":
                    SetDataType(result, DbDataTypeEnum.Path);
                    break;
                case "point":
                    SetDataType(result, DbDataTypeEnum.Point);
                    break;
                case "polygon":
                    SetDataType(result, DbDataTypeEnum.Polygon);
                    break;
                case "real":
                    SetDataType(result, DbDataTypeEnum.Real);
                    break;
                case "smallint":
                    SetDataType(result, DbDataTypeEnum.Smallint);
                    break;
                case "text":
                    SetDataType(result, DbDataTypeEnum.Text);
                    break;
                case "tsquery":
                    SetDataType(result, DbDataTypeEnum.Tsquery);
                    break;
                case "tsvector":
                    SetDataType(result, DbDataTypeEnum.Tsvector);
                    break;
                case "txid_snapshot":
                    SetDataType(result, DbDataTypeEnum.Txid_snapshot);
                    break;
                case "uuid":
                    SetDataType(result, DbDataTypeEnum.Uuid);
                    break;
                case "xml":
                    SetDataType(result, DbDataTypeEnum.Xml);
                    break;
            }
        }

        private void ParseVariableDataTypes(string str, ColumnDataInfo result)
        {
            var m = characterVaryingRegex.Match(str);
            if (m.Success)
            {
                result.DataType = DbDataTypeEnum.CharacterVarying;

                string fieldSizeStr = m.Groups["field_size"].Value;

                if (!String.IsNullOrWhiteSpace(fieldSizeStr))
                {
                    result.FieldSize = Int32.Parse(fieldSizeStr);
                }
            }

            m = bitRegex.Match(str);
            if (m.Success)
            {
                result.DataType = DbDataTypeEnum.Bit;

                string fieldSizeStr = m.Groups["field_size"].Value;

                if (!String.IsNullOrWhiteSpace(fieldSizeStr))
                {
                    result.FieldSize = Int32.Parse(fieldSizeStr);
                }
            }

            m = bitVaryingRegex.Match(str);
            if (m.Success)
            {
                result.DataType = DbDataTypeEnum.BitVarying;

                string fieldSizeStr = m.Groups["field_size"].Value;

                if (!String.IsNullOrWhiteSpace(fieldSizeStr))
                {
                    result.FieldSize = Int32.Parse(fieldSizeStr);
                }
            }

            m = charRegex.Match(str);
            if (m.Success)
            {
                result.DataType = DbDataTypeEnum.Character;

                string fieldSizeStr = m.Groups["field_size"].Value;

                if (!String.IsNullOrWhiteSpace(fieldSizeStr))
                {
                    result.FieldSize = Int32.Parse(fieldSizeStr);
                }
            }

            m = numericRegex.Match(str);
            if (m.Success)
            {
                result.DataType = DbDataTypeEnum.Numeric;

                string fieldSizeStr = m.Groups["field_size"].Value;
                string precision = m.Groups["precision"].Value;

                if (!String.IsNullOrWhiteSpace(fieldSizeStr) && !String.IsNullOrWhiteSpace(precision))
                {
                    result.FieldSize = Int32.Parse(fieldSizeStr);
                    result.Precision = Int32.Parse(precision);
                }
            }

            m = timeRegex.Match(str);
            if (m.Success)
            {
                result.DataType = DbDataTypeEnum.TimeWithTimeZone;

                string fieldSizeStr = m.Groups["field_size"].Value;

                if (!String.IsNullOrWhiteSpace(fieldSizeStr))
                {
                    result.FieldSize = Int32.Parse(fieldSizeStr);
                }
            }

            m = timeWtzRegex.Match(str);
            if (m.Success)
            {
                result.DataType = DbDataTypeEnum.TimeWithoutTimeZone;

                string fieldSizeStr = m.Groups["field_size"].Value;

                if (!String.IsNullOrWhiteSpace(fieldSizeStr))
                {
                    result.FieldSize = Int32.Parse(fieldSizeStr);
                }
            }

            m = timestampRegex.Match(str);
            if (m.Success)
            {
                result.DataType = DbDataTypeEnum.TimestampWithTimeZone;

                string fieldSizeStr = m.Groups["field_size"].Value;

                if (!String.IsNullOrWhiteSpace(fieldSizeStr))
                {
                    result.FieldSize = Int32.Parse(fieldSizeStr);
                }
            }

            m = timestampWtzRegex.Match(str);
            if (m.Success)
            {
                result.DataType = DbDataTypeEnum.TimestampWithoutTimeZone;

                string fieldSizeStr = m.Groups["field_size"].Value;

                if (!String.IsNullOrWhiteSpace(fieldSizeStr))
                {
                    result.FieldSize = Int32.Parse(fieldSizeStr);
                }
            }
        }

        #endregion
    }
}
